#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

# ====ERROR HANDLE==== 
try:
    print('try...')
    r = 10 / 0 + int('a') 
    print('result:', r) # because error happens here, print() in the next line will not be executed
except ValueError as e:
    print('except:', e) # this will bot be executed, because only one error will be raised
except UnicodeError as e: # all errors are class inherited from class BaseException
    print('UnicodeError') # and UnicodeError is inherited from ValueError, so this line will never execute
except ZeroDivisionError as e:
    print('except:', e) # 'except: division by zero'
else:
    print('no error!') # if not error happens, this will execute.
finally: # no matter if the error happens or not, finally: will be executed in the end.
    print('finally...')
print('END')
# this try...except can cross multiple layers of invoking

# if the error is not catched, it will reach the intepreter, print the error invoke stack, and end the run
'''
Traceback (most recent call last):
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
'''
# the original error is at the bottom of stack.

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e) # to avoid ending of the run, we catch the error 
        # and use logging to print error stack or save error log

main()
print('END') # since the error is catched, the run continue, this line will execute

# we can def our own error class based on build-in error class
class FooError(ValueError):
    pass

def foo2(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s) # and raise our own error
    return 10 / n

foo2('0')
# but only def error class if neccesary. use build-in errors if possible.

def foo3(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar3():
    try:
        foo3('0')
    except ValueError as e:
        print('ValueError!')
        raise # this will raise again the catched error.
    # if the func here cannot handle the error, the best approach is to raise it to higher layer 
    # (but log it firstly)

bar3()

# ====DEBUG==== 
# one of simple ways to examine program running is to use print()
# but there are more optimized options:
n = 0
assert n != 0, 'n is zero!' # use assert to examine boolean expression
# if False, it will throw AssertionError: 'your comment'
# and run cmd in python -O err.py with -O (uppercase o) can run with ignoring asserts

import logging
logging.basicConfig(level=logging.INFO) # level can determine which level of logging message will print
# level: INFO - DEBUG - WARNNING - ERROR

s = '0'
n = int(s)
logging.info('n = %d' % n) # logging.info will log a message with severity 'INFO'
print(10 / n)

# in cmd, run python with -m pdb can enter pdb 'single step debug' environment
## type 'l' to see the script
## type 'n' to run next line
## type 'p [variable name]' to see value of any variable
## type 'q' to exit debugging

import pdb

pdb.set_trace() # add this line to set a breakpoint
# when running to the breakpoint, python will automatically enter pdb environment
## type 'c' to continue running

# OR USE IDE TO DEBUG!

# ====UNIT TEST==== 
'''
如果你听说过“测试驱动开发”（TDD：Test-Driven Development），单元测试就不陌生。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()，我们可以编写出以下几个测试用例：
    输入正数，比如1、1.2、0.99，期待返回值与输入相同；
    输入负数，比如-1、-1.2、-0.99，期待返回值与输入相反；
    输入0，期待返回0；
    输入非数值类型，比如None、[]、{}，期待抛出TypeError。
把上面的测试用例放到一个测试模块里，就是一个完整的单元测试。
如果单元测试通过，说明我们测试的这个函数能够正常工作。
如果单元测试不通过，要么函数有bug，要么测试条件输入不正确，总之，需要修复使单元测试能够通过。
单元测试通过后有什么意义呢？如果我们对abs()函数代码做了修改，只需要再跑一遍单元测试，
如果通过，说明我们的修改不会对abs()函数原有的行为造成影响，
如果测试不通过，说明我们的修改与原有行为不一致，要么修改代码，要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。
在将来修改的时候，可以极大程度地保证该模块行为仍然是正确的。
'''
class Dict(dict): # we def a class Dict inherited from dict and add a feature make it can fetch .attr

    def __init__(self, **kw):
        super().__init__(**kw)

    def __getattr__(self, key): # get value according to key. Dict.key == Dict[key]
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value): # set key-value couples of Dict
        self[key] = value

import unittest # import unit test package

class TestDict(unittest.TestCase): # to test, a new class inherited from unittest.TestCase is defined

    def test_init(self): # method begin with test_ is test method, that will be used in test
        d = Dict(a=1, b='test')
        self.assertEqual(d.a, 1) # assert two value are equal
        self.assertEqual(d.b, 'test')
        self.assertTrue(isinstance(d, dict)) # assert a boolean expression is True

    def test_key(self):
        d = Dict()
        d['key'] = 'value'
        self.assertEqual(d.key, 'value')

    def test_attr(self):
        d = Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEqual(d['key'], 'value')

    def test_keyerror(self):
        d = Dict()
        with self.assertRaises(KeyError): # assert specific kind of error will be raised
            value = d['empty'] # .assertRaises must be used after 'with'

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty
    
    def setUp(self): # setUp method will execute every time before a test_method execute
        print('setUp...')

    def tearDown(self): # setUp method will execute every time after a test_method execute
        print('tearDown...')

if __name__ == '__main__': # if this conditional is added, directly run the script will start test
    unittest.main()

# without the conditional above, run 'python -m unittest mydict_test' in cmd is recommanded
'''
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单，如果测试代码太复杂，那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了，但是不通过程序肯定有bug。
'''

# ====DOC TEST==== 
# doctest module can extract test code with >>> in comment and run these test code
class Dict2(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict2()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict2(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict2' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__': # only when the script is directly run in cmd:
    import doctest
    doctest.testmod()



